local global = _G;
local lxp = lxp;
local io = io;
local table = table;
local pairs = pairs;
local assert = assert;
local type = type;
local setmetatable = setmetatable;
local string = string;
local error = error;
local tonumber = tonumber;

local print = print; -- DEBUGGING

module(...)

local nilSelfErrorMessage = "XML object 'self' is nil. Did you use '.' instead of ':'?";

Node = { };
Node.__index = Node;

-- create a new node
function Node.create(_name, _attributes)
	local tmp = {};
	setmetatable(tmp, Node);
	tmp._NAME = _name;
	tmp._ATTRIBUTES = _attributes;

	return tmp;
end


-- Write a node to a file
function Node:write(filename)
	local outfile, err = io.open(filename, "w");
	if( outfile == nil ) then
		error(err, 2);
	end

	local level = 0;

	local function write_subnodes(node)
		node:debug();

		if( level > 0 ) then
			outfile:write("\n");
		end

		outfile:write(string.rep("\t", level) .. "<" .. node._NAME);
		local i = 1;
		while(true) do
			local name = node._ATTRIBUTES[i];
			if( name == nil ) then
				break;
			end;

			local value = node:getAttribute(name);
			i = i + 1;

			outfile:write(" " .. name .. "=\"" .. value .. "\"");
		end

		local elements = node:getElements();

		if( node._VALUE or #elements > 0 ) then
			outfile:write(">");
		else
			outfile:write(" />");
		end


		if( node._VALUE ) then
			outfile:write(node._VALUE);
		end

		level = level + 1;
		for i,v in pairs(elements) do
			write_subnodes(v);
		end
		level = level - 1;

		if( node._VALUE or #elements > 0 ) then
			if( #elements > 1 or (node._VALUE and string.find(node._VALUE, "\n")) ) then
				outfile:write("\n" .. string.rep("\t", level) .. "</" .. node._NAME .. ">");
			else
				outfile:write("</" .. node._NAME .. ">");
			end

			if( level > 0 ) then
				outfile:write("\n");
			end
		end
	end

	write_subnodes(self);

end

local function explicitCast(_value, _type)
	-- If no type is given, don't cast.
	if( _type == nil ) then return _value; end;

	if( _type == "string" ) then
		return global.tostring(_value);
	elseif( _type == "number" ) then
		return global.tonumber(_value);
	elseif( _type == "boolean" ) then
		if( _value == "true" ) then return true; end;
		if( _value == "false" ) then return false; end;
		return (global.tonumber(_value)) ~= 0;
	else
		return _value;
	end;
end


-- get a single element by it's name or index
function Node:getElement(index, forcetype)
	if( type(self) ~= "table" ) then error(nilSelfErrorMessage, 2) end;

	if( type(index) == "string" ) then
		for i,v in pairs(self) do
			if( v._NAME == index and i ~= "_ATTRIBUTES" ) then
				return explicitCast(v, forcetype);
			end
		end
		return nil; -- not found
	end

	if( type(index) == "number" ) then
		if( index < 1 ) then return nil; end; -- invalid index
		if( index > #self ) then return nil; end; -- invalid index (not 100% accurate, but quick)

		local count = 0;
		for i,v in pairs(self) do
			if( type(v) == "table" and i ~= "_ATTRIBUTES" ) then
				count = count + 1;
				if( count == index ) then
					return explicitCast(v, forcetype);
				end
			end
		end
		return nil; -- not found
	end

	return nil; -- invalid index type, return nil
end

-- get all elements as a table
function Node:getElements()
	if( type(self) ~= "table" ) then error(nilSelfErrorMessage, 2) end;

	local tmp = {};
	for i,v in pairs(self) do
		if( type(v) == "table" and i ~= "_ATTRIBUTES" ) then
			table.insert(tmp, v);
		end
	end

	return tmp;
end

-- get the number of elements that are a child of this node
function Node:getElementCount()
	if( type(self) ~= "table" ) then error(nilSelfErrorMessage, 2) end;

	local count = 0;
	for i,v in pairs(self) do
		if( type(v) == "table" ) then
			count = count + 1;
		end
	end

	return count;
end

function Node:getAttribute(index, forcetype)
	if( type(self) ~= "table" ) then error(nilSelfErrorMessage, 2) end;

	if( type(index) == "string" or type(index) == "number") then
		return explicitCast(self._ATTRIBUTES[index], forcetype);
	end

	return nil;
end

function Node:getAttributes()
	if( type(self) ~= "table" ) then error(nilSelfErrorMessage, 2) end;

	return self._ATTRIBUTES;
end

function Node:getAttributeCount()
	if( type(self) ~= "table" ) then error(nilSelfErrorMessage, 2) end;

	return #self._ATTRIBUTES;
end

-- return _NAME
function Node:getName()
	if( type(self) ~= "table" ) then error(nilSelfErrorMessage, 2) end;

	return self._NAME;
end

-- return _VALUE
function Node:getValue(forceType)
	if( type(self) ~= "table" ) then error(nilSelfErrorMessage, 2) end;

	return explicitCast(self._VALUE, forcetype);
end

-- print a debug string of the current node
function Node:debug()
	if( type(self) ~= "table" ) then error(nilSelfErrorMessage, 2) end;

	local str = string.format("Element \'%s\'", self._NAME);
	print(str);

	for i,v in pairs(self) do
		str = string.format("  %s:", i);
		print(str, v)
	end

	str = string.format("\n%s._ATTRIBUTES:", self._NAME);
	print(str);
	for i,v in pairs(self._ATTRIBUTES) do
		str = string.format("  %s:", i);
		print(str, v);
	end
end


function implicitCast(data)
	-- True or false
	if( data == "true" ) then
		return true; end;

	if( data == "false" ) then
		return false; end;

	-- Check if is a valid number
	--if( string.find(data, "^[%-%+]?[0-9]+%.?[0-9]+$") ~= nil ) then
	if( string.find(data, "^[%-%+]?%d+%.?%d+$") ~= nil ) then
		return tonumber(data);
	end

	if( string.find(data, "^%d+$") ~= nil ) then
		return tonumber(data);
	end

	-- Check if is a valid hexidecimal value
	if( string.find(data, "^(0x%x+)$") ~= nil) then
		return tonumber(data:sub(3), 16);
	end

	-- Assume it is a string
	local retstr = global.tostring(data);

	-- Check if it's an empty string, return nil if it is
	if( retstr == "" ) then
		return nil;
	end;

	return retstr;
end


function startElement(parser, _name, _attributes)
	local stack = parser:getcallbacks().stack;

	for i,v in pairs(_attributes) do
		_attributes[i] = implicitCast(_attributes[i]);
	end

	local newtab = Node.create(_name, _attributes);
	table.insert(stack, newtab);
end

function endElement(parser, _name)
	local stack = parser:getcallbacks().stack;
	local element = table.remove(stack);
	local level = #stack;
	table.insert(stack[level], element);
end

function characterData(parser, str)
	--if( not string.find(str, "%w") ) then
	--  return;
	--end

	str = implicitCast(str);

	local stack = parser:getcallbacks().stack;
	local element = stack[#stack];
	local n = #element;

	if( element._VALUE ) then
		--element._VALUE = element._VALUE .. "\n" .. str;
		element._VALUE = element._VALUE .. str;
	else
		element._VALUE = str;
	end
end


function parse(input, haltOnError)
	if( haltOnError == nil ) then haltOnError = true; end;
	if( input == nil ) then
		error("xml.parse error: Input stream is nil", 2);
	end;

	local callbacks = {
		StartElement = startElement,
		EndElement = endElement,
		CharacterData = characterData,
		_nonstrict = true,
		stack = {{}}
	};

	local p = lxp.new(callbacks);
	--p:setencoding("ISO-8859-1");

	local result, msg, line, col, pos;
	if( type(input) == "userdata" ) then
		-- Prase an already opened file that was opened with io.open()
		for l in input:lines() do
			result, msg, line, col, pos = p:parse(l);
			if( not result ) then
				break;
			end
			p:parse("\n");
		end

		if( result ) then
			p:close();
			--return callbacks.stack[1][1];
		else
			--return result, msg, line, col, pos;
		end
	elseif( type(input) == "string" ) then
		-- Parse a string
		result, msg, line, col, pos = p:parse(input);
	else
		-- Invalid type
		error("Invalid type passed to xml.parse()", 2);
	end

	-- If everything went OK, return callbacks.stack[1][1] (the root object)
	-- Otherwise, return error information
	if( not result ) then
		print("haltOnError: ", haltOnError);
		if( haltOnError ) then
			error("XML Parse Error." ..
				"\nStream: " .. global.tostring(input) ..
				"\nLine: " .. line ..
				"\nColumn: " .. col ..
				"\nPos: " .. pos .. 
				"\nMessage: " .. msg, 2);
		else
			return result, msg, line, col, pos;
		end
	else
		return callbacks.stack[1][1];
	end
end


function open(filename, haltOnError)
	if( haltOnError == nil ) then haltOnError = true; end;

	if( type(filename) ~= "string" ) then
		if( haltOnError ) then
			local err = string.format("Filename must be given to xml.open, received: %s\n", type(filename));
			error(err, 2);
		else
			return false;
		end
	end

	local file = io.open(filename);
	if( file == nil ) then
		if( haltOnError ) then
			local err = string.format("Cannot open file \'%s\' for reading.", filename);
			error(err, 0);
		else
			return false;
		end
	end

	local result, msg, line, col, pos = parse(file, false);

	if( result == nil ) then
		if( haltOnError ) then
			local fname_short;
			if( string.len(filename) > 52 ) then
				fname_short = "..." .. string.sub(filename, -52);
			else
				fname_short = filename;
			end

			error("XML Parse Error." ..
				"\nFile: " .. fname_short ..
				"\nLine: " .. line ..
				"\nColumn: " .. col ..
				"\nPos: " .. pos .. 
				"\nMessage: " .. msg, 2);
		else
			return false;
		end
	end

	file:close();
	return result;
end

--[[
function open(filename)
	if( type(filename) ~= "string" ) then
		local err = string.format("Filename must be given to xml.open, received: %s\n", type(filename));
		error(err, 2);
	end

	local callbacks = {
		StartElement = startElement,
		EndElement = endElement,
		CharacterData = characterData,
		_nonstrict = true,
		stack = {{}}
	};

	local p = lxp.new(callbacks);
	local file = io.open(filename);

	if( file == nil ) then
		local err = string.format("Cannot open file \'%s\' for reading.", filename);
		error(err, 0);
	end

	--p:setencoding("ISO-8859-1");

	local parse = {}
	for l in file:lines() do
		parse.result, parse.msg, parse.line, parse.col, parse.pos = p:parse(l);
		if (parse.result) then

		else
			local fname_short;
			if( string.len(filename) > 52 ) then
				fname_short = "..." .. string.sub(filename, -52);
			else
				fname_short = filename;
			end

			error("XML Parse Error." ..
				"\nFile: " .. fname_short ..
				"\nLine: " .. parse.line ..
				"\nColumn: " .. parse.col ..
				"\nMessage: " .. parse.msg, 2);
		end
		p:parse("\n");
	end

	p:parse();
	p:close();
	file:close();

	return callbacks.stack[1][1];
end
]]
